/*
 * Copyright 2011 Christoph Ludwig (christoph[at]chludwig.de). All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.chludwig.ehumanities.provenance.background;

import java.io.File;
import java.io.FileFilter;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.chludwig.ehumanities.provenance.entities.Archive;
import de.chludwig.ehumanities.provenance.persistence.ArchivePO;
import de.chludwig.ehumanities.provenance.persistence.StorageArea;

/**
 * Wrapper around {@link BackgroundThreadContextListener} that resubmits all unfinished zip Archive creation jobs on startup. 
 */
public class BackgroundJobScheduler implements ServletContextListener {

  private static final String MSG_SUBMITTING_ZIP_TASK = "submitting zip task for {} to the background thread executor";

  private static final String MSG_NO_PENDING_TASKS = "no archives found that need to be zipped";

  private static final String MSG_SWEEPER_SCHEDULED = "scheduled sweeper task every {}s with an initial delay of {}s";

  private static final String MSG_INITIALIZED = "background job scheduler initialized";

  private static final String MSG_FORWARDING_INITILIZED_EVENT = "forwarding context initialized event to background thread listener";

  private static final String MSG_SHUT_DOWN = "background job scheduler shut down";

  private static final String MSG_FORWARDING_DESTRYOED_EVENT = "forwarding context destroyed event to the background thread listener";

  /**
   * Key of the sweeper interval init parameter set in the web.xml file.
   */
  public static final String DE_CHLUDWIG_EHUMANITIES_PROVENANCE_SWEEPER_INTERVAL_SECONDS = "de.chludwig.ehumanities.provenance.sweeperseconds";

  /**
   * Default value of the sweeper interval init parameter.
   * 
   * If the sweeper interval is not specified in the servlet context or if the
   * parameter cannot be parsed as a positive integer, then the sweeper will be scheduled to run every 30 minutes.
   */
  public static final long DEFAULT_SWEEPER_INTERVAL_SECONDS = 1800;

  private static final String MSG_NON_POSITIVE_INTERVAL_ERROR = MessageFormat.format(
          "Servlet configuration error: Init parameter {0} must be a positive integer; fallback is {1} seconds",
          DE_CHLUDWIG_EHUMANITIES_PROVENANCE_SWEEPER_INTERVAL_SECONDS, DEFAULT_SWEEPER_INTERVAL_SECONDS);

  private static final String MSG_INTERVAL_PARSE_ERROR = MessageFormat
          .format("Servlet configuration error: Init parameter {0} cannot be parsed as a decimal integer; fallback is {1} seconds",
                  DE_CHLUDWIG_EHUMANITIES_PROVENANCE_SWEEPER_INTERVAL_SECONDS, DEFAULT_SWEEPER_INTERVAL_SECONDS);

  private static final String MSG_DEFAULT_INTERVAL_INFO = MessageFormat.format(
          "Servlet configuration: Init parameter {0} not set; fallback is {1} seconds",
          DE_CHLUDWIG_EHUMANITIES_PROVENANCE_SWEEPER_INTERVAL_SECONDS, DEFAULT_SWEEPER_INTERVAL_SECONDS);

  private static final Logger LOG = LoggerFactory.getLogger(BackgroundJobScheduler.class);

  private static final FileFilter ARCHIVE_FILTER = new FileFilter() {

    @Override
    public boolean accept(File pathname) {
      boolean accepted = false;
      if(pathname.isDirectory() && pathname.canRead()) {
        String name = pathname.getName();
        try {
          UUID archiveId = UUID.fromString(name);
          Archive archive = Archive.archiveWithId(archiveId);
          accepted = archive.zipAvailable();
        }
        catch(IllegalArgumentException ex) {
          accepted = false;
        }
      }
      return accepted;
    }
    
  };

  private BackgroundThreadContextListener backgroundThreadListener = new BackgroundThreadContextListener();
  
  private final StorageArea storageArea = new StorageArea();
  
  private StorageArea getStorageArea() {
    return storageArea;
  }
  
  /* (non-Javadoc)
   * @see javax.servlet.ServletContextListener#contextDestroyed(javax.servlet.ServletContextEvent)
   */
  @Override
  public void contextDestroyed(ServletContextEvent event) {
    LOG.info(MSG_FORWARDING_DESTRYOED_EVENT);
    backgroundThreadListener.contextDestroyed(event);
    LOG.info(MSG_SHUT_DOWN);
  }

  /* (non-Javadoc)
   * @see javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent)
   */
  @Override
  public void contextInitialized(ServletContextEvent event) {
    LOG.info(MSG_FORWARDING_INITILIZED_EVENT);
    backgroundThreadListener.contextInitialized(event);
    
    ServletContext ctx = event.getServletContext();
    schedulePendingZipTasks(ctx);
    schedulePeriodicSweeper(ctx);
    LOG.info(MSG_INITIALIZED);
  }

  /**
   * schedule a periodic sweeper task that purges all archives and workflow containers not recently modified.
   *  
   * @param ctx
   */
  private void schedulePeriodicSweeper(ServletContext ctx) {
    ScheduledExecutorService executor = backgroundThreadListener.getExecutor();
    SweeperTask sweeper = new SweeperTask(ctx);
    long sweeperInterval = getSweeperIntervalInSeconds(ctx);
    long initialDelay = sweeperInterval / 2;
    executor.scheduleAtFixedRate(sweeper, initialDelay, sweeperInterval, TimeUnit.SECONDS);
    LOG.info(MSG_SWEEPER_SCHEDULED, sweeperInterval, initialDelay);
  }

  /**
   * The maximum time the provenance tool guarantees to keep submitted data. 
   */
  private long getSweeperIntervalInSeconds(ServletContext ctx) {
    String sweeperIntervalParam = ctx.getInitParameter(DE_CHLUDWIG_EHUMANITIES_PROVENANCE_SWEEPER_INTERVAL_SECONDS);
    if (sweeperIntervalParam != null) {
      try {
        long sweeperIntervalValue = Long.parseLong(sweeperIntervalParam);
        if (sweeperIntervalValue <= 0) {
          LOG.error(MSG_NON_POSITIVE_INTERVAL_ERROR);
        }
        else {
          return sweeperIntervalValue;
        }
      }
      catch (NumberFormatException ex) {
        LOG.error(MSG_INTERVAL_PARSE_ERROR, ex);
      }
    }
    else {
      LOG.info(MSG_DEFAULT_INTERVAL_INFO);
    }
    return DEFAULT_SWEEPER_INTERVAL_SECONDS;
  }


  /**
   * Queue all pending Zip tasks.
   * 
   * @param ctx the servlet's context.
   */
  private void schedulePendingZipTasks(ServletContext ctx) {
    ScheduledExecutorService executor = backgroundThreadListener.getExecutor();
    List<ZipArchiveTask> pendingTasks = pendingZipTasks();
    if(pendingTasks.size() == 0) {
      LOG.info(MSG_NO_PENDING_TASKS);
    }
    for(ZipArchiveTask task : pendingTasks) {
      String archiveFolderPath = task.getArchiveFolder().getPath();
      LOG.info(MSG_SUBMITTING_ZIP_TASK, archiveFolderPath);
      executor.submit(task);
    }
  }

  private List<ZipArchiveTask> pendingZipTasks() {
    StorageArea storage = getStorageArea();
    List<ZipArchiveTask> result = new ArrayList<ZipArchiveTask>();
    for(UUID unzippedArchiveId : unzippedArchiveIds()) {
      ArchivePO archivePO = new ArchivePO(storage, unzippedArchiveId);
      File archiveZipFile = archivePO.getZipFile();
      ZipArchiveTask task = new ZipArchiveTask(archivePO.getArchiveFolder(), archiveZipFile);
      result.add(task);
    }
    return result;
  }

  /**
   * @param dataFolder
   * @return
   */
  private List<UUID> unzippedArchiveIds() {
    File archiveFolder = new StorageArea().getArchivesFolder();
    File[] unzippedArchives = archiveFolder.listFiles(ARCHIVE_FILTER);
    List<UUID> result;
    if (unzippedArchives == null) {
      result = Collections.emptyList();
    }
    else {
      result = new ArrayList<UUID>(unzippedArchives.length);
      for (File unzippedArchiveFolder : Arrays.asList(unzippedArchives)) {
        UUID archiveId = UUID.fromString(unzippedArchiveFolder.getName());
        result.add(archiveId);
      }
    }
    return result;
  }
}
